package git.soulbgm.security.session;

import git.soulbgm.security.pojo.LoginUser;
import git.soulbgm.utils.SpringBeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 基于Redis进行定制的会话注册
 *
 * @author SoulBGM
 * @date 2020-08-05
 */
@Slf4j
@Component
public class RedisSessionRegistry implements SessionRegistry, ApplicationListener<SessionDestroyedEvent>, InitializingBean {

    @Autowired
    private RedisConnectionFactory connectionFactory;

    /**
     * 持续时间
     */
    @Value("${spring.session.timeout:30m}")
    private Duration duration;

    /**
     * 每个用户的最大会话数
     */
    @Value("${authority.maximumSessions:1}")
    private Integer maximumSessions;

    private static final String SESSION_ID_KEY = "online:sessionid:";
    private static final String PRINCIPAL_KEY = "online:principal:";
    private static final String ONLINE_ALL_KEY = "online:all";

    protected RedisTemplate<String, SessionInformation> sessionRedisTemplate;
    protected StringRedisTemplate principalRedisTemplate;

    /**
     * 获取所有用户信息
     *
     * @return 所有用户信息
     */
    @Override
    public List<Object> getAllPrincipals() {
        Set<String> all = principalRedisTemplate.opsForSet().members(buildPrincipalsAllKey());
        if (all == null) {
            return new ArrayList<>();
        }
        return new ArrayList<>(all);
    }

    /**
     * 获取某个用户的所有会话
     *
     * @param principal              用户信息
     * @param includeExpiredSessions 是否包括过期的会话
     * @return 某个用户的所有会话信息
     */
    @Override
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        final Set<String> sessions = getSessionsByPrincipal(principal);
        if (sessions == null) {
            return Collections.emptyList();
        }
        List<SessionInformation> list = new ArrayList<>(sessions.size());
        for (String sessionId : sessions) {
            SessionInformation si = getSessionInformation(sessionId);
            if (si == null) {
                continue;
            }
            if (includeExpiredSessions || !si.isExpired()) {
                list.add(si);
            }
        }
        return list;
    }

    /**
     * 通过sessionId获得会话信息
     *
     * @param sessionId 会话ID
     * @return 会话信息
     */
    @Override
    public SessionInformation getSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        return sessionRedisTemplate.opsForValue().get(buildSessionIdKey(sessionId));
    }

    /**
     * 刷新上一个请求
     *
     * @param sessionId sessionId
     */
    @Override
    public void refreshLastRequest(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        SessionInformation si = getSessionInformation(sessionId);
        if (si != null) {
            si.refreshLastRequest();
            sessionRedisTemplate.opsForValue().set(buildSessionIdKey(sessionId), si, duration);
        }
    }

    /**
     * 注册新会话
     *
     * @param sessionId sessionId
     * @param principal 用户信息
     */
    @Override
    public void registerNewSession(String sessionId, Object principal) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        Assert.notNull(principal, "Principal required as per interface contract");
        if (log.isDebugEnabled()) {
            log.debug("Registering session " + sessionId + ", for principal " + principal);
        }
        if (getSessionInformation(sessionId) != null) {
            removeSessionInformation(sessionId);
        }
        sessionRedisTemplate.opsForValue().set(buildSessionIdKey(sessionId), new SessionInformation(principal, sessionId, new Date()), duration);
        Set<String> sessions = getSessionsByPrincipal(principal);
        if (sessions.size() >= maximumSessions) {
            Iterator<String> iterator = sessions.iterator();
            if (iterator.hasNext()) {
                String repeatSessionId = iterator.next();
                removeSessionInformation(repeatSessionId);
            }
        }
        principalRedisTemplate.opsForSet().add(buildPrincipalKey(principal), sessionId);
        principalRedisTemplate.expire(buildPrincipalKey(principal), duration.getSeconds(), TimeUnit.SECONDS);
        principalRedisTemplate.opsForSet().add(buildPrincipalsAllKey(), ((LoginUser) principal).getUsername());
        principalRedisTemplate.expire(buildPrincipalsAllKey(), duration.getSeconds(), TimeUnit.SECONDS);
    }

    /**
     * 删除会话信息
     *
     * @param sessionId sessionId
     */
    @Override
    public void removeSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        SessionInformation si = getSessionInformation(sessionId);
        if (si == null) {
            return;
        }
        if (log.isTraceEnabled()) {
            log.debug("Removing session " + sessionId + " from set of registered sessions");
        }
        sessionRedisTemplate.delete(buildSessionIdKey(sessionId));
        Set<String> sessionsUsedByPrincipal = getSessionsByPrincipal(si.getPrincipal());
        if (sessionsUsedByPrincipal == null) {
            return;
        }
        if (log.isDebugEnabled()) {
            log.debug("Removing session " + sessionId + " from principal's set of registered sessions");
        }
        sessionsUsedByPrincipal.remove(sessionId);
        String principalKey = buildPrincipalKey(si.getPrincipal());
        principalRedisTemplate.opsForSet().remove(principalKey, sessionId);
        if (sessionsUsedByPrincipal.isEmpty()) {
            // 不再需要在主体中保留对象Map
            if (log.isDebugEnabled()) {
                log.debug("Removing principal " + si.getPrincipal() + " from registry");
            }
            principalRedisTemplate.delete(principalKey);
            principalRedisTemplate.opsForSet().remove(buildPrincipalsAllKey(), getPrincipalUsername(si.getPrincipal()));
            RedisOperationsSessionRepository redisOperationsSessionRepository = SpringBeanUtil.getBean(RedisOperationsSessionRepository.class);
            redisOperationsSessionRepository.deleteById(sessionId);
        }
        if (log.isTraceEnabled()) {
            log.trace("Sessions used by '" + si.getPrincipal() + "' : " + sessionsUsedByPrincipal);
        }
    }

    /**
     * 申请销毁活动
     *
     * @param event 会话销毁事件
     */
    @Override
    public void onApplicationEvent(SessionDestroyedEvent event) {
        removeSessionInformation(event.getId());
    }

    /**
     * bean初始化执行
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        sessionRedisTemplate = new RedisTemplate<>();
        sessionRedisTemplate.setKeySerializer(new StringRedisSerializer());
        sessionRedisTemplate.setHashKeySerializer(new StringRedisSerializer());
        sessionRedisTemplate.setConnectionFactory(connectionFactory);
        sessionRedisTemplate.afterPropertiesSet();

        principalRedisTemplate = new StringRedisTemplate();
        principalRedisTemplate.setConnectionFactory(connectionFactory);
        principalRedisTemplate.afterPropertiesSet();
    }

    /**
     * 通过principal来获取全部的用户名
     *
     * @param principal 实现了UserDetails接口的对象
     * @return 某个用户的所有会话ID
     */
    private Set<String> getSessionsByPrincipal(Object principal) {
        return principalRedisTemplate.opsForSet().members(buildPrincipalKey(principal));
    }

    /**
     * 构建sessionID的Key
     *
     * @param sessionId 会话ID
     * @return Redis Key
     */
    private String buildSessionIdKey(String sessionId) {
        return SESSION_ID_KEY + sessionId;
    }

    /**
     * 构建principalAll的Key
     *
     * @return Redis Key
     */
    private String buildPrincipalsAllKey() {
        return ONLINE_ALL_KEY;
    }

    /**
     * 构建principal的Key
     *
     * @param principal 实现了UserDetails接口的对象
     * @return Redis Key
     */
    private String buildPrincipalKey(Object principal) {
        return PRINCIPAL_KEY + getPrincipalUsername(principal);
    }

    /**
     * 获得Principal的用户名
     *
     * @param principal 实现了UserDetails接口的对象
     * @return 用户名
     */
    private String getPrincipalUsername(Object principal) {
        if (principal instanceof UserDetails) {
            return ((UserDetails) principal).getUsername();
        }
        return principal.toString();
    }
}
